package de.schweigerconsulting.util.csv;

import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;

/**
 * Copyright 2011 Stefan Schweiger
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License. 
 * 
 * The CsvReader class provides the required functionality to handle CSV files.
 * 
 * The used parsing algorithm processes CSV files based on the RFC4180
 * (http://tools.ietf.org/html/rfc4180).
 * 
 * @author Stefan Schweiger
 * @version 0.2.0 (Date: 06/20/2010)
 * 
 */
public class CsvReader {
	/**
	 * The given FielReader Object to read the information out of a CSV file.
	 */
	private Reader reader;

	/**
	 * This is the field separator. It defines which character enclosed the
	 * valid data of a field.
	 */
	private char fieldSeparator;

	/**
	 * The quote char defines which character is used to parse escaped field
	 * data.
	 */
	private char quoteChar;

	/**
	 * This boolean defines if the CSV data contains a headline or not.
	 */
	private boolean headline;

	/**
	 * The list of all records.
	 * 
	 * @see CsvRecord
	 */
	private List<CsvRecord> recordList;

	/**
	 * The default value for field separator.
	 */
	private static final char DEFAULT_FIELD_SEPARATOR = ';';

	/**
	 * The default value for quote character.
	 */
	private static final char DEFAULT_QUOTE_CHAR = '"';

	/**
	 * The default value for headline (boolean).
	 */
	private static final boolean DEFAULT_HEADLINE = false;

	/**
	 * Class constructor specifying the Reader.
	 * 
	 * @param reader
	 *            The Reader which provides the data stream.
	 */
	public CsvReader(Reader reader) {
		this(reader, DEFAULT_FIELD_SEPARATOR, DEFAULT_QUOTE_CHAR,
				DEFAULT_HEADLINE);
	}

	/**
	 * Class constructor specifying the Reader and if the CSV file contains a
	 * headline.
	 * 
	 * @param reader
	 *            The Reader which provides the data stream.
	 * @param headline
	 *            Specifies if a headline is available.
	 */
	public CsvReader(Reader reader, boolean headline) {
		this(reader, DEFAULT_FIELD_SEPARATOR, DEFAULT_QUOTE_CHAR, headline);
	}

	/**
	 * Class constructor specifying the Reader and the field separator.
	 * 
	 * @param reader
	 *            The Reader which provides the data stream.
	 * @param fieldSeparator
	 *            Defines the character which separates the several fields.
	 */
	public CsvReader(Reader reader, char fieldSeparator) {
		this(reader, fieldSeparator, DEFAULT_QUOTE_CHAR, DEFAULT_HEADLINE);
	}

	/**
	 * Class constructor specifying the Reader, the field separator and if the
	 * CSV file contains a headline.
	 * 
	 * @param reader
	 *            The Reader which provides the data stream.
	 * @param fieldSeparator
	 *            Defines the character which separates the several fields.
	 * @param headline
	 *            Specifies if a headline is available.
	 */
	public CsvReader(Reader reader, char fieldSeparator, boolean headline) {
		this(reader, fieldSeparator, DEFAULT_QUOTE_CHAR, headline);
	}

	/**
	 * Class constructor specifying the Reader, the field separator and the
	 * quote character.
	 * 
	 * @param reader
	 *            The Reader which provides the data stream.
	 * @param fieldSeparator
	 *            Defines the character which separates the several fields.
	 * @param quoteChar
	 *            Defines the quote character.
	 */
	public CsvReader(Reader reader, char fieldSeparator, char quoteChar) {
		this(reader, fieldSeparator, quoteChar, DEFAULT_HEADLINE);
	}

	/**
	 * Class constructor specifying the Reader, the field separator, the quote
	 * char and if the CSV file contains a headline.
	 * 
	 * @param reader
	 *            The Reader which provides the data stream.
	 * @param fieldSeparator
	 *            Defines the character which separates the several fields.
	 * @param quoteChar
	 *            Defines the quote character.
	 * @param headline
	 *            Specifies if a headline is available.
	 */
	public CsvReader(Reader reader, char fieldSeparator, char quoteChar,
			boolean headline) {
		super();
		this.reader = reader;
		this.fieldSeparator = fieldSeparator;
		this.quoteChar = quoteChar;
		this.headline = headline;
	}

	/**
	 * Returns the field separator (default <code>;</code>).
	 * 
	 * @return The field separator (default <code>;</code>).
	 */
	public char getFieldSeparator() {
		return fieldSeparator;
	}

	/**
	 * Sets the field separator (default <code>;</code>).
	 * 
	 * @param fieldSeparator
	 *            The field separator character (default <code>;</code>).
	 */
	public void setFieldSeparator(char fieldSeparator) {
		this.fieldSeparator = fieldSeparator;
	}

	/**
	 * Returns the quote character (default <code>"</code>).
	 * 
	 * @return The quote character (default <code>"</code>).
	 */
	public char getQuoteChar() {
		return quoteChar;
	}

	/**
	 * Sets the quote character (default <code>"</code>).
	 * 
	 * @param quoteChar
	 *            The quote character (default <code>"</code>).
	 */
	public void setQuoteChar(char quoteChar) {
		this.quoteChar = quoteChar;
	}

	/**
	 * Returns <code>true</code> if the CSV file contains a headline.
	 * 
	 * @return <code>true</code> if the CSV file contains a headline. false if
	 *         not.
	 */
	public boolean hasHeadline() {
		return headline;
	}

	/**
	 * Sets the <code>headline</code> property which defines if the CSV file
	 * contains a headline.
	 * 
	 * @param headline
	 *            <code>true</code> if the CSV file contains a headline.
	 *            <code>false</code> if not.
	 */
	public void setHeadline(boolean headline) {
		this.headline = headline;
	}

	/**
	 * This method starts the parsing mechanism of the given CSV file.
	 * 
	 * @throws IOException
	 */
	public void parse() throws IOException {
		this.recordList = (new CsvParser(this.reader, this.fieldSeparator,
				this.quoteChar, this.headline)).parse();
	}

	/**
	 * This method returns a <code>List</code> of all <code>CsvRecords</code>
	 * found in the CSV file.
	 * 
	 * @return A <code>List</code> of all <code>CsvRecords</code> found in the
	 *         CSV file.
	 * @see CsvRecord
	 */
	public List<CsvRecord> readAll() {
		return this.recordList;
	}

	/**
	 * This method returns a <code>List</code> of the determined
	 * <code>CsvRecords</code> found in the CSV file by considering the start
	 * position and the requested count of results.
	 * 
	 * @param start
	 *            Defines the start position (starts at 0)
	 * @param results
	 *            Defines how much values will be returned. If the count is
	 *            greater then the existing CsvRecords, the maximum possible
	 *            number of CsvRecords will be returned.
	 * @return A <code>List</code> of all <code>CsvRecords</code> found in the
	 *         CSV file.
	 * @see CsvRecord
	 */
	public List<CsvRecord> readAll(int start, int results) {
		List<CsvRecord> resultList = new ArrayList<CsvRecord>();
		long currentPosition = 0;

		for (CsvRecord record : this.recordList) {
			if (currentPosition >= (results - start)) {
				break;
			}

			if (start >= currentPosition) {
				resultList.add(record);
			}

			currentPosition++;
		}

		return resultList;
	}

	/**
	 * Returns a <code>List</code> of all values which are found at the
	 * requested index.
	 * 
	 * @param index
	 *            The requested index position.
	 * @return A <code>List</code> of all values which are found at the
	 *         requested index.
	 * @throws IOException
	 */
	public List<String> getColumnValues(int index) throws IOException {
		List<String> resultList = new ArrayList<String>();

		for (CsvRecord record : this.recordList) {
			resultList.add(record.getString(index));
		}

		return resultList;
	}

	/**
	 * Returns a <code>List</code> of all values which are found at the
	 * requested index by considering the start position and the requested count
	 * of results.
	 * 
	 * @param index
	 *            The requested index position.
	 * @param start
	 *            Defines the start position (starts at 0)
	 * @param count
	 *            Defines how much values will be returned. If the count is
	 *            greater then the existing CsvRecords, the maximum possible
	 *            number of CsvRecords will be returned.
	 * @return A <code>List</code> of all values which are found at the
	 *         requested index.
	 * @throws IOException
	 */
	public List<String> getColumnValues(int index, int start, int results)
			throws IOException {
		List<String> resultList = new ArrayList<String>();
		long currentPosition = 0L;

		for (CsvRecord record : this.recordList) {
			if (currentPosition >= (results - start)) {
				break;
			}

			if (start >= currentPosition) {
				resultList.add(record.getString(index));
			}

			currentPosition++;
		}

		return resultList;
	}

	/**
	 * Returns a <code>List</code> of all values which are found at the
	 * requested column name.
	 * 
	 * @param columnName
	 *            The name of the column.
	 * @return A <code>List</code> of all values which are found at the
	 *         requested column name.
	 * @throws IOException
	 */
	public List<String> getColumnValues(String columnName) throws IOException {
		return getColumnValues(getColumnIndex(columnName));
	}

	/**
	 * Returns a <code>List</code> of all values which are found at the
	 * requested column name by considering the start position and the requested
	 * count of results.
	 * 
	 * @param columnName
	 *            The name of the column..
	 * @param start
	 *            Defines the start position (starts at 0)
	 * @param count
	 *            Defines how much values will be returned. If the count is
	 *            greater then the existing CsvRecords, the maximum possible
	 *            number of CsvRecords will be returned.
	 * @return A <code>List</code> of all values which are found at the
	 *         requested index.
	 * @throws IOException
	 */
	public List<String> getColumnValues(String columnName, int start,
			int results) throws IOException {
		return getColumnValues(getColumnIndex(columnName), start, results);
	}

	/**
	 * Return the index of the requested column name.
	 * 
	 * @param columnName
	 *            The name of the column.
	 * @return The index of the requested column name.
	 * @throws IOException
	 */
	public int getColumnIndex(String columnName) throws IOException {
		if (this.hasHeadline()) {
			return this.recordList.get(0).getHeadline().getColumnIndex(
					columnName);
		} else {
			throw new RuntimeException("There is no headline available");
		}
	}
}
